package net.sf.fc.io;

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

import java.io.File;
import java.nio.file.CopyOption;
import java.util.ArrayList;
import java.util.List;

import net.sf.fc.io.ch.FileCopyHelper;
import net.sf.fc.io.ch.OverwriteFileCopyHelper;
import net.sf.fc.io.ch.SafeFileCopyHelper;

import org.apache.commons.io.filefilter.IOFileFilter;

/**
 * Options class that encapsulates the options to be applied during a copy. The Options
 * class includes a reference to a {@link net.sf.fc.io.ch.FileCopyHelper}
 * object that determines the actions to take if the destination file already exists during a copy.
 * It also includes a reference to three {@link org.apache.commons.io.filefilter.IOFilter}
 * objects. One is a file copy filter, another is a directory flatten filter, and the third is
 * a directory merge filter. It has a <tt>maxDirCopyLevel</tt> integer variable that defaults to
 * {@link Options#COPY_ALL_DIRS}. <tt>maxDirFlattenFilter</tt> defaults to {@link Options#NO_FLATTEN}.
 * <tt>maxDirMergeLevel</tt> defaults to {@link Options#NO_MERGE}. <tt>copyAttributes</tt>
 * defaults to true.
 */
public final class Options {
    /**
     * Builder class for <tt>Options</tt>. This class defines defaults for all
     * the properties of <tt>Options</tt>. To build a Options object that
     * uses all of the default values, you would do the following:
     * <pre>
     * Options options = new Options.Builder().build();
     * </pre>
     * This would set the <tt>FileCopyHelper</tt> object to
     * {@link net.sf.fc.io.ch.SafeFileCopyHelper}.
     * <p>
     * To set a different <tt>FileCopyHelper</tt> object, such as
     * {@link net.sf.fc.io.ch.OverwriteFileCopyHelper},
     * do the following:
     * <pre>
     * Options options = new Options.Builder().fileCopyHelper(new OverwriteFileCopyHelper()).build();
     * </pre>
     *
     */
    public static final class Builder {
        private FileCopyHelper fileCopyHelper = new SafeFileCopyHelper();
        private IOFileFilter filecopyFilter = alwaysAcceptFilter;
        private IOFileFilter dirFlattenFilter = alwaysDenyFilter;
        private IOFileFilter dirMergeFilter = alwaysDenyFilter;
        private int maxDirCopyLevel = COPY_ALL_DIRS;
        private int maxDirFlattenLevel = NO_FLATTEN;
        private int maxDirMergeLevel = NO_MERGE;
        private boolean copyAttributes = true;
        private boolean noFollowLinks = false;
        private boolean replaceExisting = false;

        /**
         * Method that sets the <tt>FileCopyHelper</tt> object to be used by
         * <tt>Options</tt> once it is built.
         * @param fileCopyHelper The FileCopyHelper object
         * @return the Builder's this pointer
         */
        public Options.Builder fileCopyHelper(final FileCopyHelper fileCopyHelper) {
            if(fileCopyHelper == null) throw new NullPointerException("FileCopyHelper must not be null.");
            this.fileCopyHelper = fileCopyHelper;
            return this;
        }

        /**
         * Method that sets the file copy filter object, which is an implementation
         * of {@link org.apache.commons.io.filefilter.IOFileFilter}.  If the file
         * copy filter object is not null, it is evaluated for each file/directory
         * before a copy is attempted. By default, the file copy filter object is
         * null.
         *
         * @param fileCopyFilter <tt>IOFileFilter</tt> object
         * @return the Builder's this pointer
         */
        public Options.Builder fileCopyFilter(final IOFileFilter fileCopyFilter) {
            // If fileCopyFilter is null, set it to alwaysAcceptFilter so we won't have to check for null.
            this.filecopyFilter = fileCopyFilter != null ? fileCopyFilter : alwaysAcceptFilter;
            return this;
        }

        /**
         * Method that sets the directory flatten filter object, which is an
         * implementation of <tt>IOFileFilter</tt>. If the directory flatten filter
         * object is not null, it is evaluated for each source directory. If it
         * returns true, the directory is "flattened", which means the directory
         * is not created in the destination directory; its contents are copied
         * directly into the destination directory. Because the directory flatten
         * filter is evaluated only for directories, there is no need to decorate
         * this filter with a {@link org.apache.commons.io.filefilter.DirectoryFileFilter}.
         * By default, the directory flatten filter object is null.
         *
         * @param fileFlattenFilter <tt>IOFileFilter</tt> object
         * @return the Builder's this pointer
         */
        public Options.Builder dirFlattenFilter(final IOFileFilter fileFlattenFilter) {
            // If fileFlattenFilter is null, set it to alwaysDenyFilter so we won't have to check for null.
            this.dirFlattenFilter = fileFlattenFilter != null ? fileFlattenFilter : alwaysDenyFilter;
            return this;
        }

        /**
         * Method that sets the directory merge filter object, which is an
         * implementation of <tt>IOFileFilter</tt>. If the directory merge filter
         * object is not null, it is evaluated for each source directory that
         * already exists in the destination directory. If it returns true, the
         * directory is "merged", which means the contents of the source directory
         * will be merged with the destination directory. The <tt>FileCopyHelper</tt>
         * object would define behavior for any files in the destination directory
         * that already exist (overwrite, rename, etc). Because the directory merge
         * filter is evaluated only for directories, there is no need to decorate
         * this filter with a {@link org.apache.commons.io.filefilter.DirectoryFileFilter}.
         * By default, the directory merge filter object is null.
         *
         * @param fileMergeFilter <tt>IOFileFilter</tt> object
         * @return the Builder's this pointer
         */
        public Options.Builder dirMergeFilter(final IOFileFilter fileMergeFilter) {
            // If fileMergeFilter is null, set it to alwaysDenyFilter so we won't have to check for null.
            this.dirMergeFilter = fileMergeFilter != null ? fileMergeFilter : alwaysDenyFilter;
            return this;
        }

        /**
         * Method that sets the maximum directory copy level. This value determines
         * how many levels of directories that <tt>FileCopier</tt> will copy. The
         * default value is {@link Options#COPY_ALL_DIRS}. For example, if
         * the source directory has the following structure:
         * <pre>
         * GrandParent
         *      ->Uncle
         *      -> Parent
         *          ->Child
         *              ->GrandChild
         * </pre>
         * and the max directory copy level is set to 2, <tt>FileCopier</tt> will
         * copy everything in GrandParent(level 1), all files in Uncle(level 2),
         * and all files in Parent(level 2), but not the Child directory or anything
         * below it because Child is level 3.
         *
         * @param maxDirCopyLevel maximum level of directories to copy
         * @return the Builder's this pointer
         */
        public Options.Builder maxDirCopyLevel(int maxDirCopyLevel) {
            if(maxDirCopyLevel < COPY_ALL_DIRS) throw new IllegalArgumentException("Max directory copy level must be 0 or greater");
            this.maxDirCopyLevel = maxDirCopyLevel;
            return this;
        }

        /**
         * Method that sets the maximum directory flatten level. This value
         * determines how many levels of directories that <tt>FileCopier</tt> will
         * flatten. Source directories that are flattened are not created in the
         * destination directory, but the contents of the directory are. The
         * default value is {@link Options#NO_FLATTEN}. For example, if
         * the source directory has the following structure:
         * <pre>
         * GrandParent
         *      ->Uncle
         *      -> Parent
         *          ->Child
         *              ->GrandChild
         * </pre>
         * and the max directory flatten level is set to 2, <tt>FileCopier</tt> will
         * flatten GrandParent(level 1), Uncle(level 2), and Parent(level 2), but
         * not the Child directory or any directory below it because Child is level 3.
         *
         * @param maxDirFlattenLevel maximum level of directories to flatten
         * @return the Builder's this pointer
         */
        public Options.Builder maxDirFlattenLevel(int maxDirFlattenLevel) {
            if(maxDirFlattenLevel < NO_FLATTEN) throw new IllegalArgumentException("Max directory flatten level must be -1 or greater");
            this.maxDirFlattenLevel = maxDirFlattenLevel;
            return this;
        }

        /**
         * Method that sets the maximum directory merge level. This value
         * determines how many levels of existing destination directories that
         * <tt>FileCopier</tt> will merge. Existing directories that are merged
         * have their contents merged with the source directory. The default value
         * is {@link Options#NO_MERGE}. For example, if the source directory
         * has the following structure and the destination directory contains the
         * same structure:
         * <pre>
         * GrandParent
         *      ->Uncle
         *      -> Parent
         *          ->Child
         *              ->GrandChild
         * </pre>
         * and the max directory merge level is set to 2, <tt>FileCopier</tt> will
         * merge the contents of GrandParent(level 1), Uncle(level 2), and
         * Parent(level 2), but not the Child directory or any directory below it
         * because Child is level 3.
         *
         * @param maxDirMergeLevel maximum level of directories to merge
         * @return the Builder's this pointer
         */
        public Options.Builder maxDirMergeLevel(int maxDirMergeLevel) {
            if(maxDirMergeLevel < NO_MERGE) throw new IllegalArgumentException("Max directory merge level must be -1 or greater");
            this.maxDirMergeLevel = maxDirMergeLevel;
            return this;
        }

        /**
         * Method that sets whether or not to preserve the file date of source
         * files/directories that are copied. The default is true.
         * @param copyAttributes true to preserve the file date
         * @return the builder's this pointer
         */
        public Options.Builder copyAttributes(boolean copyAttributes) {
            this.copyAttributes = copyAttributes;
            return this;
        }

        /**
         * Method that sets noFollowLinks. If true, a symbolic link is copied, not
         * the file to which it links. If false, the final target of the link is
         * copied. The default value is false.
         * @param noFollowLinks
         * @return
         */
        public Options.Builder noFollowLinks(boolean noFollowLinks) {
            this.noFollowLinks = noFollowLinks;
            return this;
        }

        /**
         * Method that sets replaceExisting. If true, existing files will be
         * copied.
         * @param replaceExisting
         * @return
         */
        public Options.Builder replaceExisting(boolean replaceExisting) {
            this.replaceExisting = replaceExisting;
            return this;
        }

        /**
         * Method that creates a new <tt>Options</tt> instance.
         * @return the Options instance
         */
        public Options build() {
            return(new Options(fileCopyHelper, filecopyFilter,
                    dirFlattenFilter, dirMergeFilter, maxDirCopyLevel,
                    maxDirFlattenLevel, maxDirMergeLevel, copyAttributes,
                    noFollowLinks, replaceExisting));
        }
    }

    private final FileCopyHelper fileCopyHelper;
    private final IOFileFilter fileCopyFilter;
    private final IOFileFilter dirFlattenFilter;
    private final IOFileFilter dirMergeFilter;
    private final int maxDirCopyLevel;
    private final int maxDirFlattenLevel;
    private final int maxDirMergeLevel;
    private final boolean copyAttributes;
    private final boolean noFollowLinks;
    private final boolean replaceExisting;
    private final CopyOption[] fileCpOptions;
    private final CopyOption[] dirCpOptions;

    private volatile int hashCode = 0;

    /**
     * Constant for value that means do not flatten directories. This is the
     * default setting.
     */
    public static final int NO_FLATTEN = -1;

    /**
     * Constant for value that means do not merge directories. This is the
     * default setting.
     */
    public static final int NO_MERGE = -1;

    /**
     * Constant for value that means flatten all levels of directories.
     */
    public static final int FLATTEN_ALL_DIRS = 0;

    /**
     * Constant for value that means merge all levels of directories.
     */
    public static final int MERGE_ALL_DIRS = 0;

    /**
     * Constant for value that means copy all levels of directories. This is the
     * default setting.
     */
    public static final int COPY_ALL_DIRS = 0;

    /**
     * Default IOFileFilter for the copy file filter. Use of this means there is no
     * need to check for null.
     */
    private static final IOFileFilter alwaysAcceptFilter = new IOFileFilter() {
        @Override
        public boolean accept(File dir, String name) { return true; }
        @Override
        public boolean accept(File file) {	return true; }
    };

    /**
     * Default IOFileFilter for the flatten and merge file filter. Use of this means there is no
     * need to check for null.
     */
    private static final IOFileFilter alwaysDenyFilter = new IOFileFilter() {
        @Override
        public boolean accept(File dir, String name) { return false; }
        @Override
        public boolean accept(File file) { return false; }
    };

    /**
     * Private constructor which can only be called by {@link Options.Builder}
     * @param fileCopyHelper FileCopyHelper object this instance will use
     * @param fileCopyFilter {@link org.apache.commons.io.filefilter.IOFileFilter}
     * for determining whether or not to copy files/directories
     * @param dirFlattenFilter <tt>IOFileFilter</tt> for determining whether or
     * not to flatten directories
     * @param dirMergeFilter <tt>IOFileFilter</tt> for determining whether or not
     * to merge directories.
     * @param maxDirCopyLevel the maximum number of directory levels to copy. The
     * default is {@link Options#COPY_ALL_DIRS}
     * @param maxDirFlattenLevel the maximum level of directory levels to flatten.
     * The default is {@link Options#NO_FLATTEN}
     * @param maxDirMergeLevel the maximum level of directory levels to merge.
     * The default is {@link Options#NO_MERGE}
     * @param copyAttributes true to preserve source file dates
     */
    private Options(final FileCopyHelper fileCopyHelper, final IOFileFilter fileCopyFilter,
            final IOFileFilter dirFlattenFilter, final IOFileFilter dirMergeFilter,
            int maxDirCopyLevel, int maxDirFlattenLevel, int maxDirMergeLevel,
            boolean copyAttributes, boolean noFollowLinks, boolean replaceExisting) {

        this.fileCopyHelper = fileCopyHelper;
        this.fileCopyFilter = fileCopyFilter;
        this.dirFlattenFilter = dirFlattenFilter;
        this.dirMergeFilter = dirMergeFilter;
        this.maxDirCopyLevel = maxDirCopyLevel;
        this.maxDirFlattenLevel = maxDirFlattenLevel;
        this.maxDirMergeLevel = maxDirMergeLevel;
        this.copyAttributes = copyAttributes;
        this.noFollowLinks = noFollowLinks;
        this.replaceExisting = replaceExisting;
        fileCpOptions = initFileCpOptions();
        dirCpOptions = copyAttributes ? new CopyOption[] { COPY_ATTRIBUTES } : new CopyOption[0];
    }

    private CopyOption[] initFileCpOptions() {
        List<CopyOption> fileCpOptions = new ArrayList<>();
        if(copyAttributes) fileCpOptions.add(COPY_ATTRIBUTES);
        if(noFollowLinks) fileCpOptions.add(NOFOLLOW_LINKS);
        if(replaceExisting) fileCpOptions.add(REPLACE_EXISTING);
        return fileCpOptions.toArray(new CopyOption[0]);
    }

    public static Options createDefaultOptions() {
        return new Options.Builder().build();
    }

    public static Options createOverwriteOptions() {
        return new Options.Builder().fileCopyHelper(new OverwriteFileCopyHelper()).replaceExisting(true).build();
    }

    /**
     * Method that creates a new Options instance with the given IOFileFilter
     * instance to be used as the file copy filter. All of the other parameters
     * of the current FileCopier instance are used to construct the new Options
     * object.
     * @param fileCopyFilter IOFileFilter instance
     * @return new Options object
     */
    public Options setFileCopyFilter(IOFileFilter fileCopyFilter) {
        // If fileCopyFilter is null, set it to alwaysAcceptFilter so we won't have to check for null.
        return new Options(fileCopyHelper,
                fileCopyFilter != null ? fileCopyFilter : alwaysAcceptFilter,
                dirFlattenFilter, dirMergeFilter, maxDirCopyLevel, maxDirFlattenLevel,
                maxDirMergeLevel, copyAttributes, noFollowLinks, replaceExisting);
    }

    /**
     * Method that creates a new Options instance with the given IOFileFilter
     * instance to be used as the directory flatten filter. All of the other parameters
     * of the current Options instance are used to construct the new Options
     * object.
     * @param dirFlattenFilter IOFileFilter instance
     * @return new Options object
     */
    public Options setDirFlattenFilter(IOFileFilter dirFlattenFilter) {
        // If dirFlattenFilter is null, set it to alwaysDenyFilter so we won't have to check for null.
        return new Options(fileCopyHelper, fileCopyFilter,
                dirFlattenFilter != null ? dirFlattenFilter : alwaysDenyFilter,
                dirMergeFilter, maxDirCopyLevel, maxDirFlattenLevel,
                maxDirMergeLevel, copyAttributes, noFollowLinks, replaceExisting);
    }

    /**
     * Method that creates a new Options instance with the given IOFileFilter
     * instance to be used as the directory merge filter. All of the other parameters
     * of the current Options instance are used to construct the new Options
     * object.
     * @param dirMergeFilter IOFileFilter instance
     * @return new Options object
     */
    public Options setDirMergeFilter(IOFileFilter dirMergeFilter) {
        // If dirMergeFilter is null, set it to alwaysDenyFilter so we won't have to check for null.
        return new Options(fileCopyHelper, fileCopyFilter, dirFlattenFilter,
                dirMergeFilter != null ? dirMergeFilter : alwaysDenyFilter, maxDirCopyLevel,
                maxDirFlattenLevel, maxDirMergeLevel, copyAttributes, noFollowLinks, replaceExisting);
    }

    /**
     * Method that creates a new Options instance with the given maximum
     * directory copy level. All of the other parameters of the current Options
     * instance are used to construct the new Options object.
     * @param maxDirCopyLevel int
     * @return new Options object
     */
    public Options setMaxDirCopyLevel(int maxDirCopyLevel) {

        if(maxDirCopyLevel < COPY_ALL_DIRS) throw new IllegalArgumentException("Max directory copy level must be 0 or greater");
        return new Options(fileCopyHelper, fileCopyFilter, dirFlattenFilter,
                dirMergeFilter, maxDirCopyLevel, maxDirFlattenLevel,
                maxDirMergeLevel, copyAttributes, noFollowLinks, replaceExisting);
    }

    /**
     * Method that creates a new Options instance with the given maximum
     * directory flatten level. All of the other parameters of the current Options
     * instance are used to construct the new Options object.
     * @param maxDirFlattenLevel int
     * @return new Options object
     */
    public Options setMaxDirFlattenLevel(int maxDirFlattenLevel) {

        if(maxDirFlattenLevel < NO_FLATTEN) throw new IllegalArgumentException("Max directory flatten level must be -1 or greater");
        return new Options(fileCopyHelper, fileCopyFilter, dirFlattenFilter,
                dirMergeFilter, maxDirCopyLevel, maxDirFlattenLevel,
                maxDirMergeLevel, copyAttributes, noFollowLinks, replaceExisting);
    }

    /**
     * Method that creates a new Options instance with the given maximum
     * directory merge level. All of the other parameters of the current Options
     * instance are used to construct the new Options object.
     * @param maxDirMergeLevel int
     * @return new Options object
     */
    public Options setMaxDirMergeLevel(int maxDirMergeLevel) {

        if(maxDirMergeLevel < NO_MERGE) throw new IllegalArgumentException("Max directory merge level must be -1 or greater");
        return new Options(fileCopyHelper, fileCopyFilter, dirFlattenFilter,
                dirMergeFilter, maxDirCopyLevel, maxDirFlattenLevel,
                maxDirMergeLevel, copyAttributes, noFollowLinks, replaceExisting);
    }

    /**
     * Method that creates a new Options instance with the given preserve file
     * date preserve boolean. All of the other parameters of the current FileCopier
     * instance are used to construct the new FileCopier object.
     * @param copyAttributes boolean
     * @return new Options object
     */
    public Options setCopyAttributes(boolean copyAttributes) {

        return new Options(fileCopyHelper, fileCopyFilter, dirFlattenFilter,
                dirMergeFilter, maxDirCopyLevel, maxDirFlattenLevel,
                maxDirMergeLevel, copyAttributes, noFollowLinks, replaceExisting);
    }

    /**
     * Method that creates a new Options instance with the given noFollowLinks
     * boolean. All of the other parameters of the current Options instance
     * are used to construct the new FileCopier object.
     * @param noFollowLinks
     * @return new Options object
     */
    public Options setNoFollowLinks(boolean noFollowLinks) {

        return new Options(fileCopyHelper, fileCopyFilter, dirFlattenFilter,
                dirMergeFilter, maxDirCopyLevel, maxDirFlattenLevel,
                maxDirMergeLevel, copyAttributes, noFollowLinks, replaceExisting);
    }

    /**
     * Method that creates a new Options instance with the given replace existing
     * boolean. All of the other parameters of the current FileCopier
     * instance are used to construct the new FileCopier object.
     * @param copyAttributes boolean
     * @return new Options object
     */
    public Options setReplaceExisting(boolean replaceExisting) {

        return new Options(fileCopyHelper, fileCopyFilter, dirFlattenFilter,
                dirMergeFilter, maxDirCopyLevel, maxDirFlattenLevel,
                maxDirMergeLevel, copyAttributes, noFollowLinks, replaceExisting);
    }

    /**
     * Returns the FileCopyHelper instance in use by this instance
     * @return FileCopyHelper object
     */
    public FileCopyHelper getFileCopyHelper() { return fileCopyHelper; }

    /**
     * Returns the IOFileFilter for files/directories
     * @return IOFileFilter
     */
    public IOFileFilter getFileCopyFilter() { return fileCopyFilter; }

    /**
     * Returns the IOFileFilter for flattened directories
     * @return IOFileFilter
     */
    public IOFileFilter getDirFlattenFilter() { return dirFlattenFilter; }

    /**
     * Returns the IOFileFilter for merged directories
     * @return IOFileFilter
     */
    public IOFileFilter getDirMergeFilter() { return dirMergeFilter; }

    /**
     * Returns the max directory copy level integer value
     * @return int
     */
    public int getMaxDirCopyLevel() { return maxDirCopyLevel; }

    /**
     * Returns the max directory flatten level integer value
     * @return int
     */
    public int getMaxDirFlattenLevel() { return maxDirFlattenLevel; }

    /**
     * Returns the max directory merge level integer value
     * @return int
     */
    public int getMaxDirMergeLevel() { return maxDirMergeLevel; }

    /**
     * Returns whether or not to preserve source file dates
     * @return boolean
     */
    public boolean isCopyAttributes() { return copyAttributes; }

    /**
     * Returns whether or not to follow links
     * @return
     */
    public boolean isNoFollowLinks() { return noFollowLinks; }

    /**
     * Returns whether or not to replace existing files.
     * @return
     */
    public boolean isReplaceExisting() { return replaceExisting; }

    public CopyOption[] getFileCpOptions() { return fileCpOptions; }

    public CopyOption[] getDirCpOptions() { return dirCpOptions; }

    @Override
    public boolean equals(Object object) {
        if(!(object instanceof Options)) {
            return false;
        }
        if(object == this) {
            return true;
        }
        // Since FileCopyHelper and IOFileFilter are interfaces and we don't know if
        // they are immutable or not, just check to see if they are the same instance.
        // We need to be extra careful here because the Options object is used as a
        // key in a Map.
        Options that = (Options)object;
        return(this.fileCopyHelper == that.fileCopyHelper && this.fileCopyFilter == that.fileCopyFilter &&
                this.dirFlattenFilter == that.dirFlattenFilter && this.dirMergeFilter == that.dirMergeFilter &&
                this.maxDirCopyLevel == that.maxDirCopyLevel && this.maxDirFlattenLevel == that.maxDirFlattenLevel &&
                this.maxDirMergeLevel == that.maxDirMergeLevel && this.copyAttributes == that.copyAttributes &&
                this.noFollowLinks == that.noFollowLinks);
    }

    @Override
    public int hashCode() {
        if(hashCode == 0) {
            int result = 17;
            result = 37*result+fileCopyHelper.hashCode();
            if(fileCopyFilter != null) result = 37*result+fileCopyFilter.hashCode();
            if(dirFlattenFilter != null) result = 37*result+dirFlattenFilter.hashCode();
            if(dirMergeFilter != null) result = 37*result+dirMergeFilter.hashCode();
            result = 37*result+maxDirCopyLevel;
            result = 37*result+(maxDirFlattenLevel*7);
            result = 37*result+(maxDirMergeLevel*11);
            result = 37*result+(copyAttributes ? 29 : 0);
            result = 37*result+(noFollowLinks ? 41 : 0);
            hashCode = result;
        }
        return hashCode;
    }
}
